{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "9985f7b1",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "# Introduction to Gurobi Python API\n",
    "\n",
    "###### Gurobi Days Digital\n",
    "###### June 24, 2026\n",
    "###### Maliheh Aramon, PhD, Optimization Engineer\n",
    "###### aramon@gurobi.com"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a66b4594",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "## How to Run the Notebook locally?\n",
    "- Visit [Gurobi modeling-examples](https://github.com/Gurobi/modeling-examples) repository \n",
    "- Clone the repository which contains this notebook and other examples or download it\n",
    "by clicking [here](https://github.com/Gurobi/modeling-examples/archive/refs/heads/master.zip)\n",
    "- Navigate to folder __intro_to_gurobipy__\n",
    "- [Start Jupyter Notebook Server](https://docs.jupyter.org/en/latest/running.html#id2)\n",
    "- Open the notebook in Jupyter Notebook\n",
    "- The notebook will install the gurobipy package and other dependencies. The Gurobi pip package includes a size-limited trial license that will allow you to run the notebook"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b319a3b4",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "outputs": [],
   "source": [
    "%pip install \"gurobipy>=10.0\"\n",
    "\n",
    "# Install other dependencies\n",
    "%pip install numpy\n",
    "%pip install scipy\n",
    "%pip install pandas"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e5f5db7f",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "# Gurobi Python API\n",
    "\n",
    "Gurobi Python API, also known as _gurobipy_ is the most popular Gurobi API because it allows building the model with\n",
    "- individual variables and constraints like other Guorbi's object-oriented APIs such as C, C++, Java, and .NET\n",
    "- matrices like other Gurobi's matrix-oriented interfaces such as MATLAB and R\n",
    "- more detailed mathematical syntax like traditional modeling languages\n",
    "\n",
    "In this session, we will walk you through the basics of the Gurobi Python API."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5f92f0e2",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "## How to Install gurobipy?\n",
    "\n",
    "There are two main approaches to install gurobipy on any operating systems such as Linux, Windows, or macOS.\n",
    "\n",
    "- Pip\n",
    "- Conda\n",
    "\n",
    "Relevant Knowledge Base (KB) articles:\n",
    "- [Which Python versions are supported by Gurobi?](https://support.gurobi.com/hc/en-us/articles/360013195212)\n",
    "- [How do I install Gurobi for Python?](https://support.gurobi.com/hc/en-us/articles/360044290292-How-do-I-install-Gurobi-for-Python-)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e3f02815",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "## Optimization Models \n",
    "\n",
    "The canonical form of optimization models Gurobi can handle is given below:\n",
    "\n",
    "\\begin{align}\n",
    "\\mbox{Model P:} ~~~~ \\mbox{minimize} \\quad & x^T Q x + c^T x + d &  \\notag \\\\\n",
    "\\mbox{subject to} \\quad & Ax = b & \\notag & \\notag \\\\\n",
    "                        & x^T Q_i x + c_i^T x \\leq d_i & \\forall i \\in I \\notag \\\\\n",
    "                        & l \\leq x \\leq u & \\notag \\\\\n",
    "                        & x_j \\in \\mathbb{Z} & \\forall j \\in J \n",
    "\\end{align}\n",
    "\n",
    "In this session, we would learn how to map the above math constructs to code using gurobipy.\n",
    "\n",
    "Each mathematical model has four main elements: __Data + Decision variables + Constraints + Objective function(s)__\n",
    "\n",
    "__Data__:\n",
    "\\begin{align}\n",
    "& \\mbox{Sets:}~~ I, J & \\notag \\\\ \n",
    "& \\mbox{Coefficients:}~~ Q, c, A, Q_i, c_i \\notag &\\\\ \n",
    "& \\mbox{RHS values:}~~ b, d_i & \\notag \\\\ \n",
    "& \\mbox{Lower and upper bounds:}~~ l, u & \\notag \\\\ \n",
    "& \\mbox{Constants:}~~ d & \\notag \\\\\n",
    "& \\mbox{Operators:} & \\notag \\\\\n",
    "& ~~~~~~ \\mbox{Arithmetic}~ (+, -, *, \\div)& \\notag \\\\\n",
    "& ~~~~~~ \\mbox{Constraint operators} ~(\\geq, \\leq, =) & \\notag\n",
    "\\end{align}"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5eb49aef",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "## Simple Example\n",
    "Let us start with a simple example:\n",
    "\n",
    "\\begin{align}\n",
    "\\mbox{maximize} \\quad & x + y + 2z \\notag \\\\\n",
    "\\mbox{subject to} \\quad & x + 2y + 3z \\leq 4 \\notag \\\\\n",
    "                        & x + y \\geq 1 \\notag \\\\\n",
    "                        & x, y, z \\in \\{0, 1\\} \\notag\n",
    "\\end{align}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c51e50c5",
   "metadata": {
    "code_folding": [
     22
    ],
    "slideshow": {
     "slide_type": "fragment"
    }
   },
   "outputs": [],
   "source": [
    "# Import gurobipy package as gp for convenience\n",
    "import gurobipy as gp\n",
    "\n",
    "# GRB is the list of all Gurobi constants\n",
    "from gurobipy import GRB\n",
    "\n",
    "# Create a Gurobi environment and a model object\n",
    "with gp.Env() as env, gp.Model(\"simple-example\", env=env) as model:\n",
    "    # Define decision variables\n",
    "    x = model.addVar(vtype=GRB.BINARY, name=\"x\")\n",
    "    y = model.addVar(vtype=GRB.BINARY, name=\"y\")\n",
    "    z = model.addVar(vtype=GRB.BINARY, name=\"z\")\n",
    "\n",
    "    # Define constraints\n",
    "    model.addConstr(x + 2 * y + 3 * z <= 4, name=\"c0\")\n",
    "    model.addConstr(x + y >= 1, name=\"c1\")\n",
    "\n",
    "    # Define objective\n",
    "    model.setObjective(x + y + 2 * z, sense=GRB.MAXIMIZE)\n",
    "\n",
    "    # Optimize model\n",
    "    model.optimize()\n",
    "\n",
    "    print(\"******* Solution *******\")\n",
    "    for var in model.getVars():\n",
    "        print(f\"{var.VarName}: {var.X}\")\n",
    "    print(\"************************\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c6d94dcd",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "## Python Data Structures\n",
    "\n",
    "- Tuple: An ordered, compound grouping that cannot be modified once it is created and it is ideal for representing multi dimensional subscripts.\n",
    "    ```\n",
    "        (\"city_0\", \"city_1\")\n",
    "    ```\n",
    "- List: An ordered group, so each item is indexed. Lists can be modified by adding, deleting or sorting elements.\n",
    "    ```\n",
    "        [\"city_0\", \"city_1\", \"city_2\"]\n",
    "     ```\n",
    "- Set: An unordered group of unique elements. Sets can only be modified by adding or deleting.\n",
    "    ```\n",
    "        {\"city_0\", \"city_1\", \"city_2\"}\n",
    "    ```\n",
    "- Dictionary: A key-value pair mapping that is ideal for representing indexed data such as cost, demand, capacity.\n",
    "    ```\n",
    "        damand = {\"city_0\": 100, \"city_1\": 50, \"city_2\": 40}\n",
    "    ```"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f99590d2",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    " ## Extented Data Structures in Gurobi Python API\n",
    " \n",
    " - [tuplelist()](https://www.gurobi.com/documentation/current/refman/py_tuplelist.html)\n",
    "     - A sub-class of Python list \n",
    "     - Important methods to build sub-lists efficiently \n",
    "         - [tuplelist.select(pattern)](https://www.gurobi.com/documentation/current/refman/py_tuplelist_select.html) --> tuplelist()\n",
    "     \n",
    " \n",
    " \n",
    "- [tupledict()](https://www.gurobi.com/documentation/current/refman/py_tupledict.html)\n",
    "    - A sub-class of Python dict\n",
    "    - The keys of a tupledict() are stored as tuplelist() and the values are Gurobi variable objects\n",
    "    - Important methods to build linear expressions efficiently:\n",
    "        - [tupledict.select(pattern)](https://www.gurobi.com/documentation/current/refman/py_tupledict_select.html) --> List\n",
    "        - [tupledict.sum(pattern)](https://www.gurobi.com/documentation/current/refman/py_tupledict_sum.html) --> LinExpr()\n",
    "        - [tupledict.prod(coeff, pattern)](https://www.gurobi.com/documentation/current/refman/py_tupledict_prod.html) --> LinExpr()\n",
    "    \n",
    "    \n",
    "- [multidict()](https://www.gurobi.com/documentation/current/refman/py_multidict.html): A convenience function to define multiple dictionaries in one statement."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a5637577",
   "metadata": {
    "slideshow": {
     "slide_type": "fragment"
    }
   },
   "outputs": [],
   "source": [
    "import gurobipy as gp\n",
    "from gurobipy import GRB\n",
    "\n",
    "data = gp.tupledict(\n",
    "    [\n",
    "        ((\"a\", \"b\", \"c\"), 3),\n",
    "        ((\"a\", \"c\", \"b\"), 4),\n",
    "        ((\"b\", \"a\", \"c\"), 5),\n",
    "        ((\"b\", \"c\", \"a\"), 6),\n",
    "        ((\"c\", \"a\", \"b\"), 7),\n",
    "        ((\"c\", \"b\", \"a\"), 3),\n",
    "    ]\n",
    ")\n",
    "print(f\"data: {data}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8a45c285",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"\\nTuplelist:\")\n",
    "keys = gp.tuplelist(data.keys())\n",
    "print(f\"\\tselect: {keys.select('a', '*', '*')}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "67191848",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"\\nTupledict:\")\n",
    "print(f\"\\tselect  : {data.select('a', '*', '*')}\")\n",
    "print(f\"\\tsum     : {data.sum('*', '*', '*')}\")\n",
    "coeff = {(\"a\", \"c\", \"b\"): 6, (\"b\", \"c\", \"a\"): -4}\n",
    "print(f\"\\tprod    : {data.prod(coeff, '*', 'c', '*')}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "517262d6",
   "metadata": {
    "slideshow": {
     "slide_type": "fragment"
    }
   },
   "outputs": [],
   "source": [
    "arcs, capacity, cost = gp.multidict(\n",
    "    {\n",
    "        (\"Detroit\", \"Boston\"): [100, 7],\n",
    "        (\"Detroit\", \"New York\"): [80, 5],\n",
    "        (\"Detroit\", \"Seattle\"): [120, 4],\n",
    "        (\"Denver\", \"Boston\"): [120, 8],\n",
    "        (\"Denver\", \"New York\"): [120, 11],\n",
    "        (\"Denver\", \"Seattle\"): [120, 4],\n",
    "    }\n",
    ")\n",
    "print(\"\\nMultidict:\")\n",
    "print(f\"\\tcapacity: {capacity}\")\n",
    "print(\"\\n\")\n",
    "print(f\"\\tcost: {cost}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "87d60197",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "## [Environments](https://www.gurobi.com/documentation/current/refman/py_env2.html)\n",
    "\n",
    "```\n",
    "+--------------------------------------------+\n",
    "| Environment                                |\n",
    "| +----------------------------------------+ |\n",
    "| | Model                                  | |\n",
    "| | +------+ +-----------+ +-------------+ | | \n",
    "| | | Data | | Variables | | Constraints | | |\n",
    "| | +------+ +-----------+ +-------------+ | |\n",
    "| | +-----------+                          | |\n",
    "| | | Objective |                          | |\n",
    "| | +-----------+                          | |\n",
    "| +----------------------------------------+ |\n",
    "+--------------------------------------------+  \n",
    "```\n",
    "\n",
    "Python API has a default environment which is used by default unless a new environment is created and explicitly passed to the routines that require an environment.\n",
    "\n",
    "The main reason to create an environment is to have control over when your application starts using Gurobi and when it stops using it! \n",
    "- Using remote resources such as floating, cloud, or compute server licenses for optimization\n",
    "- Garbage collection when using Jupyter notebooks\n",
    "\n",
    "Note: It is better to create new environments via the [context manager](https://support.gurobi.com/hc/en-us/articles/4424054948881-How-do-I-manage-Gurobi-environments-in-gurobipy)."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dbd8d9ad",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "## [Model](https://www.gurobi.com/documentation/current/refman/py_model.html)\n",
    "Model building in the [Python API](https://www.gurobi.com/documentation/current/refman/py_python_api_overview.html) is object oriented. The reference manual contains [a full list of methods](https://www.gurobi.com/documentation/current/refman/py_python_api_details.html#sec:Python-details) on a model object. \n",
    "\n",
    "The signature for constructing a model object is:\n",
    "```\n",
    "Model(name=\"\", env=defaultEnv)\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "31c772ec",
   "metadata": {
    "slideshow": {
     "slide_type": "fragment"
    }
   },
   "outputs": [],
   "source": [
    "import gurobipy as gp\n",
    "\n",
    "# Build a model object with the default environment\n",
    "with gp.Model(name=\"model\") as model:\n",
    "    pass\n",
    "\n",
    "model = gp.Model(name=\"model\")\n",
    "model.dispose()\n",
    "gp.disposeDefaultEnv()\n",
    "\n",
    "# Build a model object with a new environment\n",
    "with gp.Env() as env, gp.Model(name=\"model\", env=env) as model:\n",
    "    pass\n",
    "\n",
    "env = gp.Env()\n",
    "model = gp.Model(name=\"model\", env=env)\n",
    "model.dispose()\n",
    "env.dispose()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cb757e97",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "## Decision Variables\n",
    "\n",
    "Since variables are associated with a particular model object, use the [Model.addVar()](https://www.gurobi.com/documentation/current/refman/py_model_addvar.html#pythonmethod:Model.addVar) method to create a Gurobi variable object ([Var](https://www.gurobi.com/documentation/current/refman/py_var.html)):\n",
    "```\n",
    "Model.addVar(lb=0, ub=float(\"inf\"), obj=0, vtype=GRB.CONTINUOUS, name=\"\", column=None)\n",
    "```\n",
    "\n",
    "The available variable types in Gurobi are:\n",
    "- Continuous: `GRB.CONTINUOUS`\n",
    "- General integer: `GRB.INTEGER`\n",
    "- Binary: `GRB.BINARY`\n",
    "- Semi-continuous: `GRB.SEMICONT`\n",
    "- Semi-integer: `GRB.SEMIINT`\n",
    "\n",
    "A semi-continuous variable has the property that it takes a value of 0, or a value between the specified lower and upper bounds. A semi-integer variable adds the additional restriction that the variable should take an integral value."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "81839558",
   "metadata": {
    "slideshow": {
     "slide_type": "fragment"
    }
   },
   "outputs": [],
   "source": [
    "import gurobipy as gp\n",
    "from gurobipy import GRB\n",
    "\n",
    "with gp.Model(name=\"model\") as model:\n",
    "    # Define a binary decision variable\n",
    "    x = model.addVar(vtype=GRB.BINARY, name=\"x\")\n",
    "    # Define an integer variable with lb=-1, ub=100\n",
    "    y = model.addVar(lb=-1, ub=100, vtype=GRB.INTEGER, name=\"y\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "51071459",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "## [Model.addVars()](https://www.gurobi.com/documentation/current/refman/py_model_addvars.html#pythonmethod:Model.addVar)\n",
    "\n",
    "To add multiple decision variables to the model, use the Model.addVars() method which returns a Gurobi tupledict object containing the newly created variables:\n",
    "```\n",
    "Model.addVars(*indices, lb=0.0, ub=float('inf'), obj=0.0, vtype=GRB.CONTINUOUS, name=\"\")\n",
    "```\n",
    "The first argument is indices for accessing the variables:\n",
    "- Integers\n",
    "- lists of scalars\n",
    "- tuplelist\n",
    "- generator\n",
    "\n",
    "The given name is subscripted by the index of the generator expression. \n",
    "- The names are stored as ASCII strings \n",
    "    - avoid using names that contain non-ASCII characters and spaces"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b2948b40",
   "metadata": {
    "slideshow": {
     "slide_type": "fragment"
    }
   },
   "outputs": [],
   "source": [
    "import gurobipy as gp\n",
    "from gurobipy import GRB\n",
    "\n",
    "with gp.Model(name=\"model\") as model:\n",
    "    # 3D array of binary variables\n",
    "    x = model.addVars(2, 3, 4, vtype=GRB.BINARY, name=\"x\")\n",
    "    model.update()\n",
    "    print(model.getAttr(\"VarName\", model.getVars()))\n",
    "\n",
    "    # Use arbitrary lists of immutable objects to create a tupledict of 6 variables\n",
    "    y = model.addVars([1, 5], [7, 3, 2], ub=range(6), name=[f\"y_{i}\" for i in range(6)])\n",
    "    model.update()\n",
    "    print(\"\\nVariables names, upper bounds, and indices:\")\n",
    "    for index, var in y.items():\n",
    "        print(f\"name: {var.VarName}, ub: {var.UB}, index: {index}\")\n",
    "\n",
    "    # Use arbitrary list of tuples as indices\n",
    "    z = model.addVars(\n",
    "        [(3, \"a\"), (3, \"b\"), (7, \"b\"), (7, \"c\")],\n",
    "        lb=-GRB.INFINITY,\n",
    "        ub=GRB.INFINITY,\n",
    "        name=\"z\",\n",
    "    )\n",
    "    model.update()\n",
    "    print(\"\\nVariables names and lower and upper bounds:\")\n",
    "    for index, var in z.items():\n",
    "        print(f\"name: {var.VarName}, lb: {var.LB}, ub: {var.UB}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "61214373",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "## Constraints\n",
    "Like variables, constraints are also associated with a model. Use the method [Model.addConstr()](https://www.gurobi.com/documentation/current/refman/py_model_addconstr.html) to add a constraint to a model.\n",
    "```\n",
    "Model.addConstr(constr, name=\"\")\n",
    "```\n",
    "\n",
    "`constr` is a [TempConstr](https://www.gurobi.com/documentation/current/refman/py_tempconstr.html#pythonclass:TempConstr) object that can take different types:\n",
    "\n",
    "- Linear Constraint: `x + y <= 1` \n",
    "- Ranged Linear Constraint: `x + y == [1, 3]`\n",
    "- Quadratic Constraint: `x*x + y*y + x*y <= 1`\n",
    "- Linear Matrix Constraint: `A @ x <= 1`\n",
    "- Quadratic Matrix Constraint: `x @ Q @ y <= 2`\n",
    "- Absolute Value Constraint: `x == abs_(y)`\n",
    "- Logical Constraint: `x == and_(y, z)`\n",
    "- Min or Max Constraint: `z == max_(x, y, constant=9)`\n",
    "- Indicator Constraint: `(x == 1) >> (y + z <= 5)`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "87d043c9",
   "metadata": {
    "slideshow": {
     "slide_type": "fragment"
    }
   },
   "outputs": [],
   "source": [
    "import gurobipy as gp\n",
    "from gurobipy import GRB\n",
    "\n",
    "# Add constraint \"\\sum_{i=1}^{n} x_i <= b\" for any given n and b. Assume x_i are binary variables\n",
    "n, b = 10, 4\n",
    "with gp.Model(\"model\") as model:\n",
    "    x = model.addVars(n, vtype=GRB.BINARY, name=\"x\")\n",
    "    model.addConstr(gp.quicksum(x[i] for i in range(n)) <= b, name=\"c1\")\n",
    "    model.update()\n",
    "\n",
    "    # Print the LHS, Sense, and RHS of c1\n",
    "    c1 = model.getConstrByName(\"c1\")\n",
    "    print(f\"RHS, sense = {c1.RHS}, {c1.Sense}\")\n",
    "    print(f\"row: {model.getRow(c1)}\")\n",
    "    print(\"\\n\\n\")\n",
    "\n",
    "# Add constraints \"x_i + y_j - x_i*y_j >= 3\". Asssume x_i and y_j are continuous\n",
    "n, m = 5, 4\n",
    "with gp.Model(\"model\") as model:\n",
    "    x = model.addVars(n, name=\"x\")\n",
    "    y = model.addVars(m, name=\"y\")\n",
    "\n",
    "    for i in range(n):\n",
    "        for j in range(m):\n",
    "            model.addConstr(x[i] + y[j] - x[i] * y[j] >= 3, name=f\"c_{i}{j}\")\n",
    "\n",
    "    model.update()\n",
    "\n",
    "    # Print the LHS, Sense, and RHS of all c_ij constraints\n",
    "    for c in model.getQConstrs():\n",
    "        print(f\"Name: {c.QCName}\")\n",
    "        print(f\"\\tRHS, sense = {c.QCRHS}, {c.QCSense}\")\n",
    "        print(f\"\\trow: {model.getQCRow(c)}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b49864fc",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "## [Model.addConstrs](https://www.gurobi.com/documentation/current/refman/py_model_addconstrs.html)\n",
    "\n",
    "To add multiple constraints to the model, use the Model.addConstrs() method which returns a Gurobi tupledict that contains the newly created constraints:\n",
    "\n",
    "```\n",
    "Model.addConstrs(generator, name=\"\")\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ee87d5e7",
   "metadata": {
    "slideshow": {
     "slide_type": "fragment"
    }
   },
   "outputs": [],
   "source": [
    "import gurobipy as gp\n",
    "from gurobipy import GRB\n",
    "\n",
    "# Add constraints x_i + y_j <= 1 for all (i, j). Assume x_i and y_j are binary variables\n",
    "I = range(5)\n",
    "J = [\"a\", \"b\", \"c\"]\n",
    "with gp.Model(\"model\") as model:\n",
    "    x = model.addVars(I, vtype=GRB.BINARY, name=\"x\")\n",
    "    y = model.addVars(J, vtype=GRB.BINARY, name=\"y\")\n",
    "\n",
    "    generator = (x[i] + y[j] <= 1 for i in I for j in J)\n",
    "    model.addConstrs(generator, name=\"c\")\n",
    "    model.update()\n",
    "\n",
    "    # Print constraint names\n",
    "    print(model.getAttr(\"ConstrName\", model.getConstrs()))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c6e35294",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "## Objective Function\n",
    "\n",
    "To set the model objective equal to a linear or a quadratic expression, use the [Model.setObjective()](https://www.gurobi.com/documentation/current/refman/py_model_setobjective.html) method:\n",
    "```\n",
    "Model.setObjective(expr, sense=GRB.MINIMIZE)\n",
    "```\n",
    "- expr: \n",
    "    - [LinExpr()](https://www.gurobi.com/documentation/current/refman/py_lex.html): constant + coefficient-variable pairs capturing linear terms\n",
    "    - [QuadExpr()](https://www.gurobi.com/documentation/current/refman/py_qex.html): linear expression + list of coefficient-variable-variable triples\n",
    "- sense:\n",
    "    - GRB.MINIMIZE (default) or GRB.MAXIMIZE"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4e83785a",
   "metadata": {
    "slideshow": {
     "slide_type": "fragment"
    }
   },
   "outputs": [],
   "source": [
    "import gurobipy as gp\n",
    "from gurobipy import GRB\n",
    "\n",
    "import numpy as np\n",
    "\n",
    "# Add linear objectives in the form c^Tx and quadratic objective in the form x^T Q x\n",
    "n = 10\n",
    "c = np.random.rand(n)\n",
    "Q = np.random.rand(n, n)\n",
    "\n",
    "with gp.Model(\"model\") as model:\n",
    "    x = model.addVars(n, name=\"x\")\n",
    "    linexpr = gp.quicksum(c[i] * x[i] for i in range(n))\n",
    "    # linexpr = gp.quicksum(c_i * x_i for c_i, x_i in zip(c, x.values()))\n",
    "    model.setObjective(linexpr)\n",
    "    model.update()\n",
    "\n",
    "    # Print objective expression\n",
    "    obj = model.getObjective()\n",
    "    print(f\"obj: {obj}\")\n",
    "\n",
    "with gp.Model(\"model\") as model:\n",
    "    x = model.addVars(n, name=\"x\")\n",
    "    quadexpr = 0\n",
    "    for i in range(n):\n",
    "        for j in range(n):\n",
    "            quadexpr += x[i] * Q[i, j] * x[j]\n",
    "    model.setObjective(quadexpr)\n",
    "    model.update()\n",
    "\n",
    "    # Print objective expression\n",
    "    obj = model.getObjective()\n",
    "    print(f\"\\nobj: {obj}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "82e22abf",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "## [Attributes](https://www.gurobi.com/documentation/current/refman/attributes.html)\n",
    "\n",
    "The primary mechanism for querying and modifying properties of a Gurobi object is through the attribute interface. You can see the complete set of Gurobi attributes in the reference linked above.\n",
    "\n",
    "Let us see an example of how to query useful attributes on the model object after the optimization is complete."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "475ea4d5",
   "metadata": {
    "slideshow": {
     "slide_type": "fragment"
    }
   },
   "outputs": [],
   "source": [
    "import urllib.request\n",
    "import gurobipy as gp\n",
    "from gurobipy import GRB\n",
    "\n",
    "url = \"https://raw.githubusercontent.com/Gurobi/modeling-examples/master/intro_to_gurobipy/data/glass4.mps.bz2\"\n",
    "path_to_file, _ = urllib.request.urlretrieve(url, \"./glass4.mps.bz2\")\n",
    "\n",
    "# running notebook locally:\n",
    "# with gp.read(\"data/glass4.mps.bz2\") as model:\n",
    "with gp.read(path_to_file) as model:\n",
    "    model.optimize()\n",
    "\n",
    "    print(\"****************** SOLUTION ******************\")\n",
    "    print(f\"\\tStatus       : {model.Status}\")\n",
    "    print(f\"\\tObj          : {model.ObjVal}\")\n",
    "    print(f\"\\tSolutionCount: {model.SolCount}\")\n",
    "    print(f\"\\tRuntime      : {model.Runtime}\")\n",
    "    print(f\"\\tMIPGap       : {model.MIPGap}\")\n",
    "\n",
    "    print(\"\\n\")\n",
    "    for var in model.getVars()[:20]:\n",
    "        print(f\"\\t{var.VarName} = {var.X}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ca8ac2a1",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "## [Parameters](https://www.gurobi.com/documentation/current/refman/parameter_descriptions.html)\n",
    "Parameters control the mechanics of the Gurobi Optimizer."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8d5f7434",
   "metadata": {
    "slideshow": {
     "slide_type": "fragment"
    }
   },
   "outputs": [],
   "source": [
    "import urllib.request\n",
    "import gurobipy as gp\n",
    "from gurobipy import GRB\n",
    "\n",
    "url = \"https://raw.githubusercontent.com/Gurobi/modeling-examples/master/intro_to_gurobipy/data/glass4.mps.bz2\"\n",
    "path_to_file, _ = urllib.request.urlretrieve(url, \"./glass4.mps.bz2\")\n",
    "\n",
    "# running notebook locally:\n",
    "# with gp.read(\"data/glass4.mps.bz2\") as model:\n",
    "with gp.read(path_to_file) as model:\n",
    "    model.params.Threads = 1\n",
    "    model.params.TimeLimit = 10\n",
    "    model.optimize()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "97615d56",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "## [General Constraints](https://www.gurobi.com/documentation/current/refman/constraints.html#subsubsection:GeneralConstraints)\n",
    "\n",
    "General constraints allow defining certain variable relationships easily without going to the hardship of modeling these relationships in terms of the more fundamental constraints of MIP. Capturing a single one of these general constraints can often require a large set of constraints and a number of auxiliary decision variables.\n",
    "\n",
    "- __Simple General Constraints__: \n",
    "    - $z = \\mbox{max(x, y, 3)}$: `model.addConstr(z == max_(x, y, constant=3))`\n",
    "    - $z = \\mbox{min(x, y, 3)}$: `model.addConstr(z == min_(x, y, constant=3))`\n",
    "    - $y = \\mbox{abs(x)}$: `model.addConstr(y == abs_(x))`\n",
    "    - $z = x \\land y$: `model.addConstr(z == and_(x, y))`\n",
    "    - $z = x \\lor y$: `model.addConstr(z == or_(x, y))`\n",
    "    - $z = ||x||_p, ~~ p = 0, 1, 2, \\infty$: `model.addConstr(nx == norm(x, 1.0))`\n",
    "    - indicator: \n",
    "        -  $x_0 = 1 -> x_1 + 2 x_2 + x_3 \\leq 1$\n",
    "            - `model.addGenConstrIndicator(x0, 1, x1 + 2*x2 + x3 <= 1)`\n",
    "            - `model.addConstr((x0 == 1) >> (x1 + 2*x2 + x3 <= 1))`\n",
    "    - piece-wise linear:\n",
    "        - `model.addGenConstrPWL(x, y, [0, 1, 2], [1.5, 0, 3], \"\")`\n",
    "- __Function Constraints__: \n",
    "    - $y = p_0x^n + p_1x^{n-1} + \\ldots + p_nx+ p_{n+1}$:\n",
    "        - $y = 2 x^3 + 1.5 x^2 + 1$\n",
    "          - `model.addGenConstrPoly(x, y, [2, 1.5, 0, 1])`\n",
    "    - $y = e^x$: `model.addGenConstrExp(x, y)`\n",
    "    - $y = a^x$: `model.addGenConstrExpA(x, y, a)`\n",
    "    - $y = \\ln(x)$: `model.addGenConstrLog(x, y)`\n",
    "    - $y = \\log_a(x)$: `model.addGenConstrLogA(x, y, a)`\n",
    "    - $y = \\frac{1}{1+e^{-x}}$: `model.addGenConstrLogistic(x, y)`\n",
    "    - $y = x^a$: `model.addGenConstrPow(x, y, a)`\n",
    "    - $y = \\sin(x)$: `model.addGenConstrSin(x, y)`\n",
    "    - $y = \\cos(x)$: `model.addGenConstrCos(x, y)`\n",
    "    - $y = \\tan(x)$: `model.addGenConstrTan(x, y)`\n",
    "   \n",
    "\n",
    "Gurobi will automatically add a piecewise-linear approximation of the function to the model. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5e1ba317",
   "metadata": {
    "slideshow": {
     "slide_type": "fragment"
    }
   },
   "outputs": [],
   "source": [
    "# Consider the following nonconvex nonlinear problem\n",
    "#\n",
    "#  maximize    2 x    + y\n",
    "#  subject to  exp(x) + 4 sqrt(y) <= 9\n",
    "#              x, y >= 0\n",
    "\n",
    "import gurobipy as gp\n",
    "from gurobipy import GRB\n",
    "import math\n",
    "\n",
    "with gp.Model(\"model\") as model:\n",
    "    x = model.addVar(name=\"x\")\n",
    "    y = model.addVar(name=\"y\")\n",
    "    u = model.addVar(name=\"u\")\n",
    "    v = model.addVar(name=\"v\")\n",
    "\n",
    "    # Set objective\n",
    "    model.setObjective(2 * x + y, GRB.MAXIMIZE)\n",
    "\n",
    "    # u = exp(x)\n",
    "    gcf1 = model.addGenConstrExp(x, u, name=\"gcf1\")\n",
    "    # v = y^(0.5)\n",
    "    gcf2 = model.addGenConstrPow(y, v, 0.5, name=\"gcf2\")\n",
    "    c = model.addConstr(u + 4 * v <= 9)\n",
    "\n",
    "    # Use the equal piece length approach with the length = 1e-3\n",
    "    model.Params.FuncPieces = 1\n",
    "    model.Params.FuncPieceLength = 1e-3\n",
    "\n",
    "    # Optimize the model\n",
    "    model.optimize()\n",
    "\n",
    "    print(\"****************** SOLUTION ******************\")\n",
    "    print(f\"x = {x.X}, u = {u.X}\")\n",
    "    print(f\"y = {y.X}, v = {v.X}\")\n",
    "    print(f\"Obj = {model.ObjVal}\")\n",
    "\n",
    "    # Calculate violation of exp(x) + 4 sqrt(y) <= 9\n",
    "    vio = math.exp(x.X) + 4 * math.sqrt(y.X) - 9\n",
    "    if vio < 0:\n",
    "        vio = 0\n",
    "    print(f\"Vio = {vio}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a2314369",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "## Example: Portfolio Optimization\n",
    "\n",
    "In a portfolio optimization problem, there are $n$ assets. Each asset $i$ is associated with an expected return $\\mu_i$ and each pair of assets $(i, j)$ has a covariance (risk) $\\sigma_{ij}$. The goal is to find the optimal fraction of the portfolio invested in each asset to minimize the risk of investment such that 1) the total expected return of the investment exceeds the minimum target return $\\mu_0$ and 2) the portfolio invests in at most $k \\leq n$ assets.\n",
    "\n",
    "- $x_i$: Relative investment in asset $i$\n",
    "- $y_i$: Binary variable controlling whether asset $i$ is traded\n",
    "\n",
    "\\begin{align}\n",
    "\\mbox{minimize} \\quad & \\sum_{i=1}^{n} \\sum_{j=1}^{n} \\sigma_{ij} x_i x_j & \\notag \\\\\n",
    "\\mbox{subject to} \\quad & \\sum_{i=1}^{n} \\mu_i x_i \\geq \\mu_0 & \\notag \\\\\n",
    "                        & \\sum_{i=1}^{n} x_i = 1 & \\notag \\\\\n",
    "                        & \\sum_{i=1}^{n} y_i \\leq k & \\notag \\\\\n",
    "                        & x_i \\leq y_i & i=1, \\ldots, n \\notag \\\\\n",
    "                        & 0 \\leq x_i \\leq 1 & i=1, \\ldots, n \\notag \\\\\n",
    "                        & y_i \\in \\{0, 1\\} & i=1, \\ldots, n \\notag\n",
    "\\end{align}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "18a48e9e",
   "metadata": {
    "slideshow": {
     "slide_type": "fragment"
    }
   },
   "outputs": [],
   "source": [
    "import json\n",
    "from urllib.request import urlopen\n",
    "import numpy as np\n",
    "\n",
    "url = \"https://raw.githubusercontent.com/Gurobi/modeling-examples/master/intro_to_gurobipy/data/portfolio-example.json\"\n",
    "response = urlopen(url)\n",
    "data = json.loads(response.read())\n",
    "\n",
    "n = data[\"num_assets\"]\n",
    "sigma = np.array(data[\"covariance\"])\n",
    "mu = np.array(data[\"expected_return\"])\n",
    "mu_0 = data[\"target_return\"]\n",
    "k = data[\"portfolio_max_size\"]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3d1519b2",
   "metadata": {
    "slideshow": {
     "slide_type": "fragment"
    }
   },
   "outputs": [],
   "source": [
    "import gurobipy as gp\n",
    "from gurobipy import GRB\n",
    "\n",
    "# Write the solution to a pandas.DataFrame\n",
    "import pandas as pd\n",
    "\n",
    "with gp.Model(\"term-based\") as model:\n",
    "    x = model.addVars(n, ub=1, name=\"x\")\n",
    "    y = model.addVars(n, vtype=GRB.BINARY, name=\"y\")\n",
    "\n",
    "    risk = gp.quicksum(x[i] * sigma[i, j] * x[j] for i in range(n) for j in range(n))\n",
    "    # Another approach to build the risk expression\n",
    "    # risk = gp.QuadExpr()\n",
    "    # for i in range(n):\n",
    "    #    for j in range(n):\n",
    "    #        risk.addTerms(sigma[i, j], x[i], x[j])\n",
    "    model.setObjective(risk)\n",
    "\n",
    "    expected_return = gp.quicksum(mu[i] * x[i] for i in range(n))\n",
    "    model.addConstr(expected_return >= mu_0, name=\"return\")\n",
    "\n",
    "    model.addConstr(x.sum() == 1, name=\"budget\")\n",
    "    # Another approach to build the budget constraint\n",
    "    # model.addConstr(gp.quicksum(x[i] for i in range(n)) == 1, name=\"budget\")\n",
    "\n",
    "    model.addConstr(y.sum() <= k, name=\"cardinality\")\n",
    "\n",
    "    model.addConstrs((x[i] <= y[i] for i in range(n)), name=\"is_allocated\")\n",
    "\n",
    "    model.optimize()\n",
    "\n",
    "    # Write the solution into a DataFrame\n",
    "    portfolio = [var.X for var in model.getVars() if \"x\" in var.VarName]\n",
    "    risk = model.ObjVal\n",
    "    expected_return = model.getRow(model.getConstrByName(\"return\")).getValue()\n",
    "    df = pd.DataFrame(\n",
    "        data=portfolio + [risk, expected_return],\n",
    "        index=[f\"asset_{i}\" for i in range(n)] + [\"risk\", \"return\"],\n",
    "        columns=[\"Portfolio\"],\n",
    "    )\n",
    "\n",
    "    print(df)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "48c99d33",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "## Best Practices\n",
    "\n",
    "- Do ensure the separation between your model and data\n",
    "- Do use descriptive names where appropriate\n",
    "- Exploit sparsity when creating variables/constraints - Only create variables/constraints for valid combinations\n",
    "- Don’t forget to dispose of your model and environment\n",
    "- Use the documentation"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5e8e9773",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "## [Card Game](https://www.gurobi.com/resources/optimization-gamification-introducing-the-gurobipy-card-game/)\n",
    "\n",
    "\n",
    "<img src=\"./images/card-game.png\" width=\"800\" height=\"600\" style=\"margin-left:auto; margin-right:auto\"/>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "44aff1db",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "## Resources\n",
    "- [Gurobi Python Documentation](https://www.gurobi.com/documentation/current/refman/py_python_api_overview.html)\n",
    "- [Gurobi Python Examples](https://www.gurobi.com/documentation/current/examples/python_examples.html)\n",
    "- [Gurobi Jupyter Notebook Modeling Examples](https://www.gurobi.com/jupyter_models/)\n",
    "- [Gurobi Knowledge Base](https://support.gurobi.com/hc/en-us/categories/360000840331-Knowledge-Base)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.9.7"
  },
  "vscode": {
   "interpreter": {
    "hash": "aee8b7b246df8f9039afb4144a1f6fd8d2ca17a180786b69acc140d282b71a49"
   }
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
